﻿using System;
using System.Collections.Generic;
using System.Linq;
using Accountant.Utils;

namespace Accountant.DataModel
{
  public class RandomDataGenerator : IDisposable
  {
    private readonly AccountantContext _context;
    private readonly Account _liability;
    private readonly Account _exchange;
    private readonly Currency[] _currencies;
    private readonly Account[] _commonExpenses;
    private readonly Account[] _commonIncomes;
    private readonly Account[] _commonAssets;

    private readonly Random _rnd;

    public RandomDataGenerator(AccountantContext accountantContext, int seed)
    {
      _rnd = new Random(seed);
      _context = accountantContext;
      _currencies = _context.Currencies.ToArray();
      _commonAssets = _context.Accounts.Where(acc => acc.IsAsset()).ToArray();
      _commonIncomes = _context.Accounts.Where(acc => acc.IsIncome()).ToArray();
      _commonExpenses = _context.Accounts.Where(acc => acc.IsExpense()).ToArray();
      _liability = _context.Accounts[(int)KnownAccounts.Liability];
      _exchange = _context.Accounts[(int)KnownAccounts.Exchange];
    }

    private T Rand<T>(IList<T> arr) { return arr[_rnd.Next(arr.Count)]; }
    private decimal RandNextDecimal() { return (decimal)_rnd.NextDouble() * 1000; }
    private IEnumerable<Transaction> GenerateExpense(DateTime baseDate)
    {
      var exp = Rand(_commonExpenses);
      yield return new Transaction
                     {
                       Timestamp = GenerateTimestamp(baseDate),
                       Debit = Rand(_commonAssets),
                       Credit = exp,
                       Amount = RandNextDecimal(),
                       Currency = exp.PrimaryCurrency ?? Rand(_currencies)
                     };
    }
    private IEnumerable<Transaction> GenerateIncome(DateTime baseDate)
    {
      var inc = Rand(_commonIncomes);
      yield return new Transaction
                     {
                       Timestamp = GenerateTimestamp(baseDate),
                       Debit = inc,
                       Credit = Rand(_commonAssets),
                       Amount = RandNextDecimal(),
                       Currency = inc.PrimaryCurrency ?? Rand(_currencies)
                     };
    }
    private IEnumerable<Transaction> GenerateExchange(DateTime baseDate)
    {
      var asset = Rand(_commonAssets);
      var stamp = GenerateTimestamp(baseDate);
      yield return new Transaction
                     {
                       Timestamp = stamp,
                       Debit = asset,
                       Credit = _exchange,
                       Amount = RandNextDecimal(),
                       Currency = Rand(_currencies)
                     };
      yield return new Transaction
                     {
                       Timestamp = stamp,
                       Debit = _exchange,
                       Credit = asset,
                       Amount = RandNextDecimal(),
                       Currency = Rand(_currencies)
                     };
    }
    private IEnumerable<Transaction> GenerateTransfer(DateTime baseDate)
    {
      yield return new Transaction
                     {
                       Timestamp = GenerateTimestamp(baseDate),
                       Debit = Rand(_commonAssets),
                       Credit = Rand(_commonAssets),
                       Amount = RandNextDecimal(),
                       Currency = Rand(_currencies)
                     };
    }
    private IEnumerable<Transaction> GenerateCredit(DateTime baseDate)
    {
      yield return new Transaction
                     {
                       Timestamp = GenerateTimestamp(baseDate),
                       Debit = _liability,
                       Credit = Rand(_commonAssets),
                       Amount = RandNextDecimal(),
                       Currency = Rand(_currencies)
                     };
    }

    public void Run(int totalCount, DateTime dateTime)
    {
      const int transactionsPerDay = 5;
      var daysCount = totalCount / transactionsPerDay;

      var generators =
        new Func<DateTime, IEnumerable<Transaction>>[]
          {
            GenerateExpense,
            GenerateIncome,
            GenerateExchange, 
            GenerateTransfer,
            GenerateCredit
          };

      var referenceTime = dateTime - TimeSpan.FromDays(daysCount);
      for (var i = 0; i < daysCount; i++)
      {
        var time = referenceTime + TimeSpan.FromDays(i);
        for (var j = 0; j < transactionsPerDay; j++)
        {
          var stamp = time + TimeSpan.FromDays(_rnd.NextDouble());
          var transactions = Rand(generators)(stamp);
          _context.Transactions.AddRange(transactions);
        }
      }
      _context.SaveChanges();
    }

    public void Dispose()
    {
      _context.Dispose();
    }

    private static readonly TimeSpan Epsilon = TimeSpan.FromSeconds(1.001);
    private DateTime _lastTimestamp;
    /// <summary>Gets new timestamp that is quite close to now, 
    ///   yet distinct from the previous one. </summary>
    /// <returns>Unique timestamp.</returns>
    /// <remarks>Use to prevent unconscious timestamp collisions</remarks>
    public DateTime GenerateTimestamp(DateTime? baseDate = null)
    {
      var n = baseDate ?? DateTime.UtcNow;
      if (n - _lastTimestamp < Epsilon)
      {
        _lastTimestamp += Epsilon;
      }
      else
      {
        _lastTimestamp = n;
      }
      return _lastTimestamp;
    }

  }
}